Skip to main content

libobs_wrapper\data\output/
pipeline.rs

1//! High-level validated output-pipeline construction.
2//!
3//! The builder validates the complete encoder/service graph against the selected output
4//! descriptor before it creates or mutates an OBS output. Low-level attachment remains
5//! available through [`super::ObsOutputTrait`] for callers that intentionally need it.
6
7use std::sync::Arc;
8
9use crate::{
10    capabilities::{OutputCapabilities, OutputTypeInfo},
11    context::ObsContext,
12    data::{object::ObsObjectTrait, ObsData, ObsDataPointers},
13    encoders::{audio::ObsAudioEncoder, video::ObsVideoEncoder, ObsEncoderTrait},
14    services::ObsServiceRef,
15    utils::{ObsError, ObsString},
16};
17
18use super::{ObsOutputComposition, ObsOutputRef, ObsOutputTrait};
19
20/// Builder for one validated OBS output graph.
21#[derive(Debug)]
22pub struct ObsOutputPipelineBuilder {
23    context: ObsContext,
24    output_type: OutputTypeInfo,
25    name: ObsString,
26    settings: Option<ObsData>,
27    composition: ObsOutputComposition,
28}
29
30impl ObsOutputPipelineBuilder {
31    pub(crate) fn new(
32        context: ObsContext,
33        output_type: OutputTypeInfo,
34        name: ObsString,
35        settings: Option<ObsData>,
36    ) -> Self {
37        Self {
38            context,
39            output_type,
40            name,
41            settings,
42            composition: ObsOutputComposition::new(),
43        }
44    }
45
46    pub fn video_encoder(mut self, encoder: Arc<ObsVideoEncoder>) -> Self {
47        self.composition = self.composition.with_video_encoder(encoder);
48        self
49    }
50
51    pub fn audio_encoder(mut self, mixer_idx: usize, encoder: Arc<ObsAudioEncoder>) -> Self {
52        self.composition = self.composition.with_audio_encoder(mixer_idx, encoder);
53        self
54    }
55
56    pub fn service(mut self, service: Arc<ObsServiceRef>) -> Self {
57        self.composition = self.composition.with_service(service);
58        self
59    }
60
61    /// Validates the complete graph without creating an output.
62    pub fn validate(&self) -> Result<(), ObsError> {
63        self.context
64            .runtime()
65            .ensure_same_runtime(self.output_type.runtime())?;
66
67        if let Some(settings) = self.settings.as_ref() {
68            self.context
69                .runtime()
70                .ensure_same_runtime(settings.runtime())?;
71        }
72
73        let flags = self.output_type.capability_flags();
74        let encoded = flags.contains(OutputCapabilities::ENCODED);
75        let needs_video = encoded && flags.contains(OutputCapabilities::VIDEO);
76        let needs_audio = encoded && flags.contains(OutputCapabilities::AUDIO);
77        let needs_service = flags.contains(OutputCapabilities::SERVICE);
78
79        let video_encoder = self.composition.video_encoder();
80        if needs_video && video_encoder.is_none() {
81            return Err(missing_component(&self.output_type, "a video encoder"));
82        }
83        if video_encoder.is_some() && !needs_video {
84            return Err(unexpected_component(&self.output_type, "a video encoder"));
85        }
86
87        let audio_encoders = self.composition.audio_encoders();
88        if needs_audio && audio_encoders.is_empty() {
89            return Err(missing_component(
90                &self.output_type,
91                "at least one audio encoder",
92            ));
93        }
94        if !audio_encoders.is_empty() && !needs_audio {
95            return Err(unexpected_component(&self.output_type, "audio encoders"));
96        }
97        for (mixer_idx, encoder) in audio_encoders {
98            if *mixer_idx >= libobs::MAX_AUDIO_MIXES as usize {
99                return Err(ObsError::AudioMixerIndexOutOfBounds {
100                    index: *mixer_idx,
101                    max: (libobs::MAX_AUDIO_MIXES - 1) as usize,
102                });
103            }
104            self.context
105                .runtime()
106                .ensure_same_runtime(encoder.runtime())?;
107        }
108
109        let service = self.composition.service();
110        if needs_service && service.is_none() {
111            return Err(missing_component(&self.output_type, "a service"));
112        }
113        if service.is_some() && !needs_service {
114            return Err(unexpected_component(&self.output_type, "a service"));
115        }
116
117        if let Some(encoder) = video_encoder {
118            self.context
119                .runtime()
120                .ensure_same_runtime(encoder.runtime())?;
121            if let Some(codec) = encoder.codec()? {
122                if !self.output_type.video_codecs().is_empty()
123                    && !self.output_type.supports_video_codec(&codec)
124                {
125                    return Err(ObsError::OutputPipelineUnsupportedCodec {
126                        output_id: self.output_type.id().to_owned(),
127                        media: "video".to_owned(),
128                        codec,
129                    });
130                }
131            }
132        }
133
134        for encoder in audio_encoders.values() {
135            if let Some(codec) = encoder.codec()? {
136                if !self.output_type.audio_codecs().is_empty()
137                    && !self.output_type.supports_audio_codec(&codec)
138                {
139                    return Err(ObsError::OutputPipelineUnsupportedCodec {
140                        output_id: self.output_type.id().to_owned(),
141                        media: "audio".to_owned(),
142                        codec,
143                    });
144                }
145            }
146        }
147
148        if let Some(service) = service {
149            self.context
150                .runtime()
151                .ensure_same_runtime(service.runtime())?;
152            if let Some(protocol) = service.protocol()? {
153                if !self.output_type.protocols().is_empty()
154                    && !self.output_type.supports_protocol(&protocol)
155                {
156                    return Err(ObsError::OutputPipelineUnsupportedProtocol {
157                        output_id: self.output_type.id().to_owned(),
158                        protocol,
159                    });
160                }
161            }
162        }
163
164        Ok(())
165    }
166
167    /// Validates the complete graph, creates the output, then applies the desired state.
168    /// No output is created when validation fails.
169    pub fn build(mut self) -> Result<ObsOutputPipeline, ObsError> {
170        self.validate()?;
171        let output =
172            self.context
173                .create_output(&self.output_type, self.name, self.settings.take())?;
174        output.apply_composition(self.composition)?;
175        Ok(ObsOutputPipeline { output })
176    }
177}
178
179/// A fully validated and wired output ready for lifecycle operations.
180#[derive(Clone, Debug)]
181pub struct ObsOutputPipeline {
182    output: ObsOutputRef,
183}
184
185impl ObsOutputPipeline {
186    pub fn output(&self) -> &ObsOutputRef {
187        &self.output
188    }
189
190    pub fn start(&self) -> Result<(), ObsError> {
191        self.output.start()
192    }
193
194    pub fn stop(&self) -> Result<(), ObsError> {
195        self.output.stop()
196    }
197
198    pub fn is_active(&self) -> Result<bool, ObsError> {
199        self.output.is_active()
200    }
201
202    pub fn into_output(self) -> ObsOutputRef {
203        self.output
204    }
205}
206
207impl ObsContext {
208    /// Starts a high-level pipeline builder for a discovered output type.
209    pub fn output_pipeline(
210        &self,
211        output_type: &OutputTypeInfo,
212        name: impl Into<ObsString>,
213        settings: Option<ObsData>,
214    ) -> ObsOutputPipelineBuilder {
215        ObsOutputPipelineBuilder::new(self.clone(), output_type.clone(), name.into(), settings)
216    }
217}
218
219fn missing_component(output_type: &OutputTypeInfo, component: &str) -> ObsError {
220    ObsError::OutputPipelineMissingComponent {
221        output_id: output_type.id().to_owned(),
222        component: component.to_owned(),
223    }
224}
225
226fn unexpected_component(output_type: &OutputTypeInfo, component: &str) -> ObsError {
227    ObsError::OutputPipelineUnexpectedComponent {
228        output_id: output_type.id().to_owned(),
229        component: component.to_owned(),
230    }
231}